﻿/*
 * FLAP.cs v1.3
 * Written by (c) agehack a.k.a. GUnitSoldier
 * Released under GNU GPL (http://www.gnu.org/copyleft/gpl.html)
 * 
 * Thanks to [Daniel Grunwald] for fixed NetworkBitConverter.
 * Thanks to [crackoff].
 * 
 * http://www.agehack.pro/oscar/flap
 */
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using Grunwald.NOscar;

namespace Agehack
{
    /*
     * FLAP MANAGED TRANSPORT C#
     * Packet version 1.0
    */
    public sealed class Flap
    {
        public const ushort C_MAX_SIZE = 65535;
        public const byte C_HEADER_SIZE = 6;

        #region Fields
        private byte fId = 0, fChan = 0;
        private ushort fSequenceNumber = 0;
        private byte[] fBody = null;
        #endregion
        #region Constructors
        public Flap(byte id, byte channel, ushort sequencenum, byte[] body)
        {
            fId = id;
            fChan = channel;
            fSequenceNumber = sequencenum;
            fBody = body;
        }
        public Flap(byte id, byte channel, byte[] body)
            : this(id, channel, 0, body) { }
        public Flap(byte id, byte channel)
            : this(id, channel, null) { }
        #endregion
        #region Properties
        public byte Id { get { return fId; } set { fId = value; } }
        public byte Channel { get { return fChan; } set { fChan = value; } }
        public ushort SequenceNumber { get { return fSequenceNumber; } set { fSequenceNumber = value; } }
        public byte[] Body { get { return fBody; } set { fBody = value; } }
        public ushort BodySize
        {
            get
            {
                return ((ushort)fBody.Length);
            }
        }
        #endregion

        public byte[] GetBytes()
        {
            byte[] rb = new byte[BodySize + C_HEADER_SIZE];
            rb[0] = Id; rb[1] = Channel;
            util.merge(rb, NetworkBitConverter.GetBytes(SequenceNumber), 2, out rb);
            util.merge(rb, NetworkBitConverter.GetBytes(BodySize), 4, out rb);
            util.merge(rb, Body, 6, out rb);
            return rb;
        }
        public static Flap Parse(byte[] body)
        {
            Flap p = new Flap(body[0], body[1]);
            p.SequenceNumber = NetworkBitConverter.ToUInt16(body, 2);
            ushort bodysz = NetworkBitConverter.ToUInt16(body, 4);
            p.Body = util.subarray(body, C_HEADER_SIZE, bodysz);
            return p;
        }
        public void AppendBody(byte[] add)
        {
            if (Body == null)
                Body = add;
            else
            {
                byte[] newbody = new byte[BodySize + add.Length];
                util.merge(newbody, Body, 0);
                util.merge(newbody, add, BodySize, out newbody);
                Body = newbody;
            }
        }
        public override string ToString()
        {
            return String.Format(
                "{0}:{1}#{2}", Id, Channel, SequenceNumber);
        }
    }
    public sealed class FlapConnection : IDisposable
    {
        #region Properties & Fields
        private Stream stream = null;
        private Socket clientSocket = null;
        private byte[] dataBuffer = new byte[Flap.C_MAX_SIZE];
        private Flap received = null;
        private bool raiseEvents = false;
        public bool RaiseEvents { get { return raiseEvents; } set { raiseEvents = value; } }
        public Socket Socket { get { return clientSocket; } }
        public bool IsConnected { get { return clientSocket != null && clientSocket.Connected; } }
        #endregion
        #region Constructors, datatypes, etc...
        public FlapConnection()
        {            
        }
        public FlapConnection(Socket sock)
        {
            clientSocket = sock;
        }
        public FlapConnection(Stream stream)
        {
            this.stream = stream;
        }
        ~FlapConnection() { Dispose(); }
        public void Dispose()
        {
            if (IsConnected)
                Disconnect();
        }
        #endregion
        #region Events
        public delegate void PacketEvent(FlapConnection con, Flap packet);
        public delegate void ConnectionDisconnected(FlapConnection con);
        public event PacketEvent OnPacketReceived;
        public event PacketEvent OnPacketSent;
        public event ConnectionDisconnected OnDisconnected;
        private void onPackReceived()
        {
            if (raiseEvents && OnPacketReceived != null)
            {
                OnPacketReceived(this, received);
            }
        }
        private void onPackSent(Flap f)
        {
            if (raiseEvents && OnPacketSent != null)
            {
                OnPacketSent(this, f);
            }
        }
        private void onDiscon() { if (raiseEvents && OnDisconnected != null) OnDisconnected(this); }
        #endregion
        #region Tasks on socket
        public void Connect(IPEndPoint ip)
        {
            if (IsConnected)
                throw new InvalidOperationException("Already connected.");

            try
            {
                if (clientSocket == null)
                    clientSocket = new Socket(
                        AddressFamily.InterNetwork,
                        SocketType.Stream, ProtocolType.Tcp);
                clientSocket.Connect(ip);
                callRecv();
            }
            catch (SocketException ex)
            {
                throw new IOException("Connect() failed with socket excpetion code " + ex.ErrorCode, ex);
            }
        }
        public void Disconnect()
        {
            clientSocket.Shutdown(SocketShutdown.Both);
        }
        private void dataReceived(IAsyncResult ar)
        {
            Socket curSock = null;
            int bytesRead = -2;
            IPEndPoint rep = null;
            try
            {
                curSock = (Socket)ar.AsyncState;
                bytesRead = curSock.EndReceive(ar);
                rep = (IPEndPoint)curSock.RemoteEndPoint;
            }
            catch (SocketException)
            {
                Disconnect();
                onDiscon();
            }

            if (curSock != null && bytesRead > 0)
            {
                received = null;
                try
                {
                    received = Flap.Parse(dataBuffer);
                    if (raiseEvents)
                        onPackReceived();
                }
                catch { received = null; }

                callRecv();
            }
            else
                onDiscon();
        }
        private void callRecv()
        {
            if (IsConnected)
                clientSocket.BeginReceive(dataBuffer, 0, Flap.C_MAX_SIZE, SocketFlags.None,
                    new AsyncCallback(dataReceived), clientSocket);
        }
        public Flap Receive(int timeout_seconds)
        {
            if (!IsConnected)
                throw new InvalidOperationException("No active socket to receive data from.");

            while (timeout_seconds > 0)
            {
                if (received != null)
                {
                    Flap f = received;
                    received = null;
                    return f;
                }

                timeout_seconds--;
                System.Threading.Thread.Sleep(1000);
            }
            return null;
        }
        #region System.IO.Stream mode support
        // Thise methods are made for server stream connection support.
        // You can create FlapConnection(TcpClient.GetStream()) on server
        // And work with him without events, using this 2 methods.
        // None of another methods in FlapConnection are supported if you working with stream.
        public Flap StreamRead()
        {
            if (stream == null)
                throw new InvalidOperationException("This is not Stream-type connection.");

            try
            {
                byte[] header = new byte[Flap.C_HEADER_SIZE];
                stream.Read(header, 0, Flap.C_HEADER_SIZE);
                ushort bodysz = BitConverter.ToUInt16(header, 4);
                byte[] flap = new byte[Flap.C_HEADER_SIZE + bodysz];
                util.merge(flap, header, 0, out flap);
                stream.Read(flap, header.Length, bodysz);
                return Flap.Parse(flap);
            }
            catch
            {
                return null;
            }
        }
        public void StreamWrite(Flap f)
        {
            if (stream == null)
                throw new InvalidOperationException("This is not Stream-type connection.");

            try
            {
                byte[] buf = f.GetBytes();
                stream.Write(buf, 0, buf.Length);
                stream.Flush();
            }
            catch
            {
            }
        }
        #endregion
        private void Write(byte[] sendData)
        {
            if (!IsConnected)
                throw new InvalidOperationException("No active socket to write data to.");

            clientSocket.Send(sendData);
        }       
        public void Send(Flap pack)
        {
            if (pack.BodySize > Flap.C_MAX_SIZE - Flap.C_HEADER_SIZE)
                throw new ArgumentException("Packet was too large to be sent in single packet. For file transfers and etc. use dataTransfer.");

            Write(pack.GetBytes());
            onPackSent(pack);
        }
        #endregion
    }

    // Internal..
    public static partial class util
    {
        public static void merge(byte[] src, byte[] append, int from_pos, out byte[] res)
        {
            int ipos = from_pos;
            for (int i = 0; i < append.Length; i++)
            {
                src[ipos] = append[i];
                ipos++;
            }
            res = src;
        }
        public static byte[] merge(byte[] src, byte[] append, int from_pos)
        {
            byte[] bts = null;
            merge(src, append, from_pos, out bts);
            return bts;
        }
        public static byte[] subarray(byte[] src, int from_pos, int count)
        {
            byte[] result = new byte[count];
            int src_pos = from_pos;
            for (int result_pos = 0; result_pos < count; result_pos++, src_pos++)
                result[result_pos] = src[src_pos];
            return result;
        }
        public static byte[] subarray(byte[] src, int start_pos)
        {
            return subarray(src, start_pos, src.Length - start_pos);
        }
        public static byte[] receive(Stream s, int count)
        {
            byte[] bts = new byte[count];
            int read = 0;
            while (read < count)
            {
                bts[read] = ((byte)s.ReadByte());
                read++;
            }
            return bts;
        }

        public static string strUnpack(int encd, byte[] packed)
        {
            return Encoding.GetEncoding(encd).GetString(packed);
        }
        public static byte[] strPack(int encd, string unpacked)
        {
            return Encoding.GetEncoding(encd).GetBytes(unpacked);
        }

        public static string bytesPack(byte[] array)
        {
            string strPacked = String.Empty;
            foreach (byte b in array)
                strPacked += byteStr(b);
            return strPacked;
        }
        public static byte[] bytesUnpack(string strPacked)
        {
            byte[] unp = new byte[strPacked.Length / 3];
            int pos = 0;
            for (int i = 0; i != unp.Length; i += 3)
            {
                if (i >= strPacked.Length) break;
                string bs = strPacked[i].ToString();
                bs += strPacked[i + 1];
                bs += strPacked[i + 2];
                byte b = Convert.ToByte(bs);
                unp[pos] = b;

                pos++;
            }
            return unp;
        }
        private static string byteStr(byte b)
        {
            string bs = b.ToString();
            while (bs.Length < 3)
                bs = "0" + bs;
            return bs;
        }
    }
}
